Enable MSAA with OIT - #25345
Open
cormacrelf wants to merge 4 commits into
Open
Conversation
Remove the check_msaa system that panicked when an OIT camera had MSAA enabled, and make the OIT resolve pass MSAA-compatible. This is really basic support and has artifacts on triangle edges.
With MSAA, the fragment shader runs once per triangle covering any sample of a pixel, so pixels on shared mesh edges get one OIT fragment per adjacent triangle. The OIT buffers store fragments per pixel, not per sample, so both invocations blended at full coverage, double-blending the surface along every interior triangle edge and drawing a visible wireframe pattern on smooth meshes. Or something like that. This fixes it, with an API change to oit_draw. oit_draw now always takes the @Builtin(sample_mask) fragment input. Each OitFragmentNode stores the sample_mask. Node size increases from 12 -> 16 bytes, +33% memory use on the nodes buffer. The resolve shader takes @Builtin(sample_index), which forces it to run at sample rate. Each sample walks the pixel's list compositing only fragments that cover it, and hardware MSAA averages the results. Adjacent triangles now composite exactly at any alpha with antialiasing. As long as you aren't stacking too many transparent triangles to be sorted correctly. I only tested this with a backport to bevy 17, and overflowing the sort brings about some kind of mismatch in the order between two samples which can appear as visible mesh edges again. Nevertheless this works a lot of the time. The cost is MSAA 4 -> ~4x resolve pass invocations, which are expensive. Takes up more lanes. It's up to the user. We could make it zero cost for Msaa::Off by compiling in a different OitFragmentNode type.
With a depth prepass provided, oit_draw tested prepass_depth at sample 0 and culled the whole fragment on failure. That's obviously no good under MSAA. A fragment might be occluded at only some of the samples. We can describe this with a reduced sample mask, so we test each sample for prepass depth and clear the bits that are occluded.
Contributor
|
Welcome, new contributor! Please make sure you've read our contributing guide, as well as our policy regarding AI usage, and we look forward to reviewing your pull request shortly ✨ |
beicause
reviewed
Aug 10, 2026
| color: u32, | ||
| depth_alpha: u32, | ||
| next: u32, | ||
| sample_mask: u32, |
Member
There was a problem hiding this comment.
sample_mask should be gated behind @if to avoid wasting memory when MSAA is off.
Author
There was a problem hiding this comment.
I have actually done this, it's just a messy change and wasn't sure. I'll push it too
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Objective
Enable MSAA in combination with Order Independent Transparency. Currently bevy panics if you enable both because OIT does not take MSAA into account.
Solution
Keep the single linked list per output pixel, but add a bitmask to each node. We scale up the resolver to the MSAA sample rate, and when resolving and walking the list, we skip nodes whose triangle didn't cover that sample point and keep walking. With depth prepass, we also mask off any fully occluded samples in oit_draw so the resolver never finds any nodes for those samples.
Testing
The OIT example runs with all combinations of settings, including new msaa setting
Tested with a backport to bevy 17 in a fairly involved application with a lot of transparency. Native linux and WebGPU.
Ultimately left it disabled because we have so much transparency it overflows too much. Hopefully the newer linked list version lets us use it when we upgrade.
Needs a bit of attention to the depth prepass probably, I don't understand the depth pass that well and in bevy 17 I couldn't enable depth prepass without lots of artifacts (it was disabled before). That may have been our custom shader behaving badly though.
Will need release notes and migration guide for the breaking API change to
oit_draw. If you like it I'll write em. Something like this@fragment fn fragment( in: VertexOutput, ... + @if(MATERIAL_OIT_ENABLED) + @builtin(sample_mask) sample_mask: u32, ) -> FragmentOutput { // ... @if(MATERIAL_OIT_ENABLED) { - oit_draw(in.position, color); + oit_draw(in.position, color, sample_mask); discard; } ... }Showcase
Naive approach gives you...
And the fixed version with MSAA all the way through the OIT shaders: